package com.michaelbulava.TFS.vcs;

import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.options.Configurable;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vcs.*;
import com.intellij.openapi.vcs.annotate.AnnotationProvider;
import com.intellij.openapi.vcs.changes.ChangeListManager;
import com.intellij.openapi.vcs.changes.ChangeProvider;
import com.intellij.openapi.vcs.checkin.CheckinEnvironment;
import com.intellij.openapi.vcs.diff.DiffProvider;
import com.intellij.openapi.vcs.history.VcsHistoryProvider;
import com.intellij.openapi.vcs.rollback.RollbackEnvironment;
import com.intellij.openapi.vcs.update.UpdateEnvironment;
import com.intellij.openapi.vcs.versionBrowser.ChangeBrowserSettings;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.vcsUtil.VcsUtil;
import com.michaelbulava.TFS.log.FileLogger;
import com.michaelbulava.TFS.tasks.connect.InitializeModuleWorkspaceTask;
import com.michaelbulava.TFS.tpignore.Line;
import com.michaelbulava.TFS.tpignore.TPIgnoreCache;
import com.michaelbulava.TFS.tpignore.TPIgnoreDocument;
import com.michaelbulava.TFS.vcs.configuration.TFSProjectConfigurable;
import com.michaelbulava.TFS.vcs.exceptions.NotConnectedToTFS;
import com.michaelbulava.TFS.vcs.internal.environments.TFSCheckinEnvironment;
import com.michaelbulava.TFS.vcs.internal.environments.TFSUpdateEnvironment;
import com.michaelbulava.TFS.vcs.internal.providers.*;
import com.michaelbulava.TFS.vcs.utils.TFSFileUtil;
import com.microsoft.tfs.core.clients.versioncontrol.soapextensions.Workspace;
import com.microsoft.tfs.core.clients.versioncontrol.workspacecache.WorkspaceInfo;
import com.microsoft.tfs.util.Check;
import org.jetbrains.annotations.Nullable;

import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Created by mbulava on 5/6/2015.
 */
public class TFSVcs extends AbstractVcs<TFSCommittedChangeList> {
    public static final String VCS_NAME = "tfs";
    public static final String DISPLAY_NAME = "Team Foundation Server";
    private static final Logger log = FileLogger.getInstance(TFSVcs.class);
    private static final VcsKey ourKey = createKey(VCS_NAME);

    private final Map<Module, TPIgnoreCache> ignoreCacheMap = new HashMap<Module, TPIgnoreCache>();

    private final VcsShowConfirmationOption myAddConfirmation;
    private final VcsShowConfirmationOption myDeleteConfirmation;
    private final VcsShowSettingOption myCheckoutOptions;

    private CheckinEnvironment myCheckinEnvironment;
    private UpdateEnvironment myUpdateEnvironment;
    private CommittedChangesProvider myCommittedChangesProvider;
    private AnnotationProvider myAnnotationProvider;
    private VcsHistoryProvider myHistoryProvider;
    private DiffProvider myDiffProvider;

    private VcsVFSListener myFileListener;

    private final Object workspaceListLock = new Object();
    private final Map<Module, Workspace> moduleWorkspaces = new HashMap<Module, Workspace>();
    private final Map<Workspace, Boolean> workspaceConnected = new HashMap<Workspace, Boolean>();

    private final List<RevisionChangedListener> myRevisionChangedListeners = ContainerUtil.createLockFreeCopyOnWriteList();
    private int connectinCount =0;

    private int initWorkspaceCount = 0;
    private int initWorkspaceInProgress = 0;

    public TFSVcs(Project project){
        super(project, VCS_NAME);

        ProjectLevelVcsManager vcsManager = ProjectLevelVcsManager.getInstance(project);
        myAddConfirmation = vcsManager.getStandardConfirmation(VcsConfiguration.StandardConfirmation.ADD, this);
        myDeleteConfirmation = vcsManager.getStandardConfirmation(VcsConfiguration.StandardConfirmation.REMOVE, this);
        myCheckoutOptions = vcsManager.getStandardOption(VcsConfiguration.StandardOption.CHECKOUT, this);

    }

    @Override
    public void activate(){
        myFileListener = new TFSVcsVFSListener(myProject, this);

        //myFileChangeListener = new FileChangeListener(myProject);
        //VirtualFileManager.getInstance().addVirtualFileListener(myFileChangeListener);
//        ApplicationManager.getApplication().invokeLater(new Runnable() {
//            @Override
//            public void run() {
//                ConnectProject();
//            }
//        });
    }

    @Override
    public void deactivate(){
        Disposer.dispose(myFileListener);
        //VirtualFileManager.getInstance().removeVirtualFileListener(myFileChangeListener);
        //myFileChangeListener = null;
        //DisconnectProject();
    }

    @Override
    public String getDisplayName() {
        return DISPLAY_NAME;
    }

    @Override
    public ChangeProvider getChangeProvider(){
        return new TFSChangeProvider(myProject);
    }

    @Override
    public Configurable getConfigurable() {
        return new TFSProjectConfigurable(myProject);
    }

    @Override
    public CheckinEnvironment createCheckinEnvironment(){
        if (myCheckinEnvironment == null)
            myCheckinEnvironment = new TFSCheckinEnvironment(this);
        return myCheckinEnvironment;
    }

    @Nullable
    public CheckinEnvironment getCheckinEnvironment() {
        return myCheckinEnvironment;
    }

    @Override
    public UpdateEnvironment createUpdateEnvironment(){
        if (myUpdateEnvironment == null)
            myUpdateEnvironment = new TFSUpdateEnvironment(this);
        //TODO UpdateEnvironment needs some work.
        return myUpdateEnvironment;
    }

    @Override
    public UpdateEnvironment getUpdateEnvironment() {
        return myUpdateEnvironment;
    }

    @Override
    public CommittedChangesProvider<TFSCommittedChangeList, ChangeBrowserSettings> getCommittedChangesProvider(){
        if (myCommittedChangesProvider == null)
            myCommittedChangesProvider = new TFSCommitedChangesProvider2(this);
        Check.notNull(myCommittedChangesProvider, "getCommittedChangesProvider");
        return myCommittedChangesProvider;
    }

    @Override
    public EditFileProvider getEditFileProvider(){
        return new TFSEditFileProvider(myProject);
    }

    @Override
    public AnnotationProvider getAnnotationProvider(){
        if (myAnnotationProvider == null)
            myAnnotationProvider = new TFSAnnotationProvider(this);
        return myAnnotationProvider;
    }

    @Override
    public VcsHistoryProvider getVcsHistoryProvider(){
        if(myHistoryProvider == null)
            myHistoryProvider = new TFSHistoryProvider(myProject);
        return myHistoryProvider;
    }

    @Override
    public DiffProvider getDiffProvider(){
        if (myDiffProvider == null)
            myDiffProvider = new TFSDiffProvider(myProject);
        return myDiffProvider;
    }

    @Override
    public RollbackEnvironment createRollbackEnvironment(){
        //TODO create RollbackEnvironment
        return super.createRollbackEnvironment();
    }

    @Override
    public RollbackEnvironment getRollbackEnvironment(){
        return super.getRollbackEnvironment();
    }

    public boolean isIgnoredFile(FilePath file){
        if (file.getVirtualFile() != null)
            return isIgnoredFile(file.getVirtualFile());
        else if (file.getPath() != null){
            VirtualFile vf = VcsUtil.getVirtualFile(file.getPath());
            if (vf == null){
                while (vf == null && FileUtil.isAncestor(new File(myProject.getBasePath()), file.getIOFile(), false)){
                    file = file.getParentPath();
                    vf = VcsUtil.getVirtualFile(file.getPath());;
                }
            }

            if (vf != null)
                return isIgnoredFile(vf);
            else
                return false;
        }
        else
            return true;
    }

    public boolean isIgnoredFile(VirtualFile file){
        try {
            ChangeListManager changeListManager = ChangeListManager.getInstance(this.myProject);
            if (changeListManager.isIgnoredFile(file))
                return true;
            Module fileModule = TFSFileUtil.getModuleForFile(file, myProject);
            if (fileModule == null)
                return true;

            synchronized (ignoreCacheMap) {
                TPIgnoreCache cache = ignoreCacheMap.get(fileModule);

                if (cache == null) {
                    cache = new TPIgnoreCache(fileModule);
                    ignoreCacheMap.put(fileModule, cache);
                }

                return cache.matchesAnyPattern(file);
            }
        } catch (Throwable tr){
            log.error("Error Checking for ignored files", tr);
            return false;

        }
    }

    public boolean addIgnoreFile(VirtualFile file){
        ChangeListManager changeListManager = ChangeListManager.getInstance(this.myProject);
        if (changeListManager.isIgnoredFile(file)){
            return false;
        }

        Module fileModule = TFSFileUtil.getModuleForFile(file, myProject);
        if (fileModule == null){
            return false;
        }

        VirtualFile ignoreFile = TPIgnoreCache.getIgnoreFile(fileModule);
        try {
            TPIgnoreDocument doc = TPIgnoreDocument.read(ignoreFile);
            doc.addLine(new Line(TPIgnoreCache.createIgnorePatternForVirtualFile(file, fileModule)));
            doc.write(ignoreFile, null);
            return true;
        } catch (IOException e) {
            return false;
        }
    }

    public static boolean isIgnored(Project project, FilePath file){
       return isIgnored(project, file.getVirtualFile());
    }

    public static boolean isIgnored(Project project, VirtualFile file){
        return getInstance(project).isIgnoredFile(file);
    }

    public boolean isWorkspaceOnline(Workspace workspace){
        Boolean val = workspaceConnected.get(workspace);
        if (val != null)
            return val.booleanValue();
        return false;
    }

    public boolean isModuleOnline(Module module){
        synchronized(workspaceListLock){
            while(isConnecting()){
                try {
                    workspaceListLock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            if(moduleWorkspaces.containsKey(module)){
                Workspace workspace = moduleWorkspaces.get(module);
                if(workspace != null){
                    return isWorkspaceOnline(workspace);
                }
            }
            return false;
        }
    }

    public boolean isModuleOnline(FilePath file){
        return isModuleOnline(TFSFileUtil.getModuleForFile(file, myProject));
    }

    public boolean isModuleOnline(VirtualFile file){
        return isModuleOnline(TFSFileUtil.getFilePath(file));
    }

    public Workspace getWorkspaceForFile(FilePath file) throws NotConnectedToTFS {
        return getWorkspaceForModule(TFSFileUtil.getModuleForFile(file, myProject));
    }

    public Workspace getWorkspaceForFile(VirtualFile file) throws NotConnectedToTFS {
        return getWorkspaceForFile(TFSFileUtil.getFilePath(file));
    }

    public synchronized Workspace getModuleTFSConnection(Module module) throws NotConnectedToTFS {
        while(isConnecting()){
            synchronized (workspaceListLock){
                while(isConnecting()){
                    try {
                        workspaceListLock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        Workspace workspace;
        synchronized (workspaceListLock){
            if (!moduleWorkspaces.containsKey(module))
                return null;
            workspace = moduleWorkspaces.get(module);
        }
        if (workspace == null && moduleWorkspaces.containsKey(module))
            throw new NotConnectedToTFS(module);

        return workspace;
    }

    public boolean isConnecting(){
        //return this.connectinCount > 0;
        return initWorkspaceCount > 0;
    }

    public void fireRevisionChanged() {
        for(RevisionChangedListener listener : myRevisionChangedListeners){
            listener.revisionChanged();
        }
    }

    public void addRevisionChangedListener(RevisionChangedListener revisionChangedListener) {
        myRevisionChangedListeners.add(revisionChangedListener);
    }

    public void removeRevisionChangedListener(RevisionChangedListener listener) {
        myRevisionChangedListeners.remove(listener);
    }

    public static Workspace getWorkspaceForModule(Module module) throws NotConnectedToTFS {
        Check.notNull(module, "module");
        Check.notNull(module.getProject(), "module.getProject()");
        return TFSVcs.getInstance(module.getProject()).getModuleTFSConnection(module);
    }

    public static TFSVcs getInstance(Project project){
        return (TFSVcs)ProjectLevelVcsManager.getInstance(project).findVcsByName(VCS_NAME);
    }

    public static void assertTrue(boolean condition){
        assertTrue(condition, "");
    }

    public static void assertTrue(boolean condition, String message){
        log.assertTrue(condition, message);
        if (!condition)
            error(message);
    }

    public static void error(String message){
        log.error(message);
        throw new RuntimeException((new StringBuilder()).append("Assertion failed:").append(message).toString());
    }

    public static VcsKey getKey() {
        return ourKey;
    }

    public static boolean isUnderTFS(FilePath path, Project project){
        AbstractVcs vcs = VcsUtil.getVcsFor(project, path);
        return vcs != null && VCS_NAME.equals(vcs.getName());
    }

    public static boolean isUnderTFS(VirtualFile path, Project project){
        AbstractVcs vcs = VcsUtil.getVcsFor(project, path);

        return vcs != null && (VCS_NAME.equals(vcs.getName()) || vcs instanceof TFSVcs);
    }

    public static void registerModuleForTFS(Module module, WorkspaceInfo workspaceInfo, boolean moduleOffline){
        Check.notNull(module, "module");
        Check.notNull(workspaceInfo, "workspaceInfo");
        Check.notNull(module.getProject(), "module.getProject()");
        TFSVcs.getInstance(module.getProject()).initializeModuleWorkspace(module, workspaceInfo, moduleOffline);
    }

    public void initializeModuleWorkspace(Module module, WorkspaceInfo workspaceInfo, boolean moduleOffline){
        Check.notNull(module, "module");
        Check.notNull(workspaceInfo, "workspaceInfo");
        initWorkspaceCount++;
        InitializeModuleWorkspaceTask task = new InitializeModuleWorkspaceTask(module, workspaceInfo, moduleOffline,
                new AssociationCallbacks());
        ProgressManager.getInstance().run(task);
    }

    @Deprecated
    public static void registerModuleForTFSOffline(Module module){
        Check.notNull(module, "module");
        TFSVcs.getInstance(module.getProject()).moduleWorkspaces.put(module, null);
    }

    private class AssociationCallbacks implements InitializeModuleWorkspaceTask.ModuleWorkspaceInitializeCallbacks{

        @Override
        public Collection<Workspace> getAssociatedWorkspaces() {
            return moduleWorkspaces.values();
        }

        @Override
        public boolean isWorkspaceOnline(Workspace workspace) {
            Boolean value = workspaceConnected.get(workspace);
            if (value != null)
                return value.booleanValue();
            else
                return false;
        }

        @Override
        public void TaskStarting() {
            while(initWorkspaceInProgress > 0){
                synchronized (workspaceListLock){
                    try {
                        workspaceListLock.wait(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            initWorkspaceInProgress++;
        }

        @Override
        public void TaskCompleted(Module module, Workspace workspace, boolean isOnline) {
            //decrement work in progress count so next connection can start.
            synchronized (workspaceListLock){
                initWorkspaceInProgress--;

                moduleWorkspaces.put(module, workspace);
                workspaceConnected.put(workspace, isOnline);
                //decrement initialization count so that when it's 0 other methods can continue.
                initWorkspaceCount--;
                //Perform notification on all waiting locks
                workspaceListLock.notifyAll();
            }
        }


    }
}
